import sys
import time
import traceback
import json
import os

import logzero
import serial
import serial.tools.list_ports
import stackprinter
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (
    QApplication,
    QCheckBox,
    QComboBox,
    QDesktopWidget,
    QDoubleSpinBox,
    QGridLayout,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QPushButton,
    QVBoxLayout,
    QWidget,
    QRadioButton,
)

from devices.mengy_ks833 import KS833_PACK_GEN, KS833_OutputStatus
from helpers.mengy_bytes_helper import bytesPuttyPrint
from qt_custom_widget.qt_helper import addTooltipInSpinBox

BAUD_LIST = (1200, 2400, 4800, 9600, 19200, 38400, 56000, 57600, 115200)
UI_INPUT_NAMES = ("UA", "UB", "UC", "IA", "IB", "IC")
QUEUE_TASK_NUM = 14

try:
    CRASH_FILE = "Ks833Ctrl_crash.log"
    CRASH_LOGGER = logzero.setup_logger("Ks833Ctrl.crash", logfile=CRASH_FILE)
except Exception:
    CRASH_LOGGER = logzero.logger


# when app raises uncaught exception, print info
def trap_exc_during_debug(exc_type, exc_value, exc_traceback):
    CRASH_LOGGER.exception("crash", exc_info=(exc_type, exc_value, exc_traceback))
    CRASH_LOGGER.error("stackprinter | {}\n".format(stackprinter.format((exc_type, exc_value, exc_traceback))))
    traceback.print_exception(exc_type, exc_value, exc_traceback)


sys.excepthook = trap_exc_during_debug

KS833_COMMON_ITEM_CONF = (
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 300, 180, 60),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 1, 1, 1, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 1, 1, 1, 300, 180, 60),
    KS833_OutputStatus(110, 110, 110, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(110, 110, 110, 0, 240, 120, 5, 5, 5, 300, 180, 60),
    KS833_OutputStatus(110, 110, 110, 0, 240, 120, 1, 1, 1, 0, 240, 120),
    KS833_OutputStatus(110, 110, 110, 0, 240, 120, 1, 1, 1, 300, 180, 60),
    KS833_OutputStatus(44, 44, 44, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(44, 44, 44, 0, 240, 120, 5, 5, 5, 300, 180, 60),
    KS833_OutputStatus(44, 44, 44, 0, 240, 120, 1, 1, 1, 0, 240, 120),
    KS833_OutputStatus(44, 44, 44, 0, 240, 120, 1, 1, 1, 300, 180, 60),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
    KS833_OutputStatus(220, 220, 220, 0, 240, 120, 5, 5, 5, 0, 240, 120),
)

KS833_COMMON_CONF_FILE_PATH = "./config/ks833_ctrl.json"
if os.path.exists(KS833_COMMON_CONF_FILE_PATH) and os.path.isfile(KS833_COMMON_CONF_FILE_PATH):
    try:
        with open(KS833_COMMON_CONF_FILE_PATH, "r") as f:
            dd = json.load(f)
        KS833_COMMON_ITEM_CONF = [KS833_OutputStatus(**i) for i in dd]
    except Exception:
        print(stackprinter.format())
else:
    try:
        basedir = os.path.dirname(os.path.abspath(KS833_COMMON_CONF_FILE_PATH))
        os.makedirs(basedir, exist_ok=True)
        with open(KS833_COMMON_CONF_FILE_PATH, "w", encoding="utf-8") as f:
            json.dump([ob._asdict() for ob in KS833_COMMON_ITEM_CONF], f, ensure_ascii=False, indent=4)
    except Exception:
        print(stackprinter.format())


class KS833Ctrler(QWidget):
    def __init__(self, app=None, logger=logzero.logger):
        super().__init__()
        self.app = app
        self.logger = logger
        self.serialObj = serial.Serial(port=None)
        self.ks833Gen = KS833_PACK_GEN(logger)
        self.uiValueWs = []
        self.uiAngleWs = []
        self.uiEnableWs = []
        self.commonSetWs = []
        self.commonSetFlag = False
        self.commonIndex = -1
        self.initUI()

    def initUI(self):
        """initiates application UI"""
        self.createInputGroupBox()
        self.createCommonSetGroupBox()
        self.createSerialWidget()
        self.setWindowTitle("KS833控制器")
        self.setGeometry(200, 200, 400, 400)
        layout = QGridLayout(self)
        layout.addWidget(self.inputGroupBox, 0, 0, 5, 2)
        layout.addWidget(self.commonSetGroupBox, 0, 2, 7, 1)
        layout.addWidget(self.serialWidget, 5, 0, 2, 2)
        self.setWindowIcon(QIcon("./icons/safebox.png"))
        self.show()
        self.center()

    def center(self):
        """centers the window on the screen"""
        screen = QDesktopWidget().screenGeometry()
        size = self.geometry()
        self.move((screen.width() - size.width()) / 2, (screen.height() - size.height()) / 2 - 40)

    def createInputGroupBox(self):
        self.inputGroupBox = QGroupBox("&电压电流设置")
        layout = QVBoxLayout(self.inputGroupBox)
        for idx, name in enumerate(UI_INPUT_NAMES):
            tempLayout = QHBoxLayout()
            # 名称
            ew = QCheckBox("&{}".format(name))
            ew.setChecked(True)
            tempLayout.addWidget(ew)
            self.uiEnableWs.append(ew)
            # 幅值
            vw = QDoubleSpinBox()
            if idx < 3:
                vw.setDecimals(3)
                vw.setSingleStep(1)
                vw.setSuffix("V")
                addTooltipInSpinBox(vw, (0, 900))
                vw.setValue(220)
            else:
                vw.setDecimals(5)
                vw.setSingleStep(0.1)
                vw.setSuffix("A")
                addTooltipInSpinBox(vw, (0, 30))
                vw.setValue(1)
            tempLayout.addWidget(vw)
            self.uiValueWs.append(vw)
            # 角度
            aw = QDoubleSpinBox()
            aw.setSuffix("°")
            aw.setDecimals(3)
            aw.setSingleStep(1)
            addTooltipInSpinBox(aw, (0, 360))
            aw.setValue((0, 240, 120)[idx % 3])
            self.uiAngleWs.append(aw)
            tempLayout.addWidget(aw)
            layout.addLayout(tempLayout)
        self.singleShotButton = QPushButton("开始输出", self.inputGroupBox)
        self.singleShotButton.setCheckable(True)
        self.singleShotButton.clicked[bool].connect(self.on_singleShot)
        layout.addWidget(self.singleShotButton)

    def getInputConf(self):
        """读取电压电流输入值"""
        # 相别选择
        phase_num = 0
        for i, ew in enumerate(self.uiEnableWs):
            if ew.checkState():
                phase_num += 1 << i
        self.ks833Gen.phase_num = phase_num
        Ua_Rms, Ub_Rms, Uc_Rms, Ia_Rms, Ib_Rms, Ic_Rms = [i.value() for i in self.uiValueWs]
        Ua_Ang, Ub_Ang, Uc_Ang, Ia_Ang, Ib_Ang, Ic_Ang = [i.value() for i in self.uiAngleWs]
        ks833_status = KS833_OutputStatus(
            Ua_Rms, Ub_Rms, Uc_Rms, Ua_Ang, Ub_Ang, Uc_Ang, Ia_Rms, Ib_Rms, Ic_Rms, Ia_Ang, Ib_Ang, Ic_Ang
        )
        return ks833_status

    def _serial_write(self, pack):
        """串口发送数据"""
        if not self.serialObj.isOpen():
            try:
                self.serialObj.open()
            except Exception:
                self.logger.error("open port failed | {}".format(stackprinter.format()))
                return False
        try:
            self.serialObj.write(pack)
        except Exception:
            self.logger.error("send pack failed | {}".format(stackprinter.format()))
            return False
        finally:
            self.serialObj.close()
        return True

    def gen_output_send_pack(self, new_status: KS833_OutputStatus):
        """处理档位切换"""
        if self.ks833Gen.is_switch_range(new_status):  # 需要重新切换档位
            pack = self.ks833Gen.pack_shutdown_output()  # 关闭输出
            self.logger.debug("switch range send shutdown pack | {}".format(bytesPuttyPrint(pack)))
            self._serial_write(pack)
            time.sleep(2)
            self.ks833Gen.conf_output_status(new_status)  # 重新配置输出值
            send_pack = self.ks833Gen.pack_start_output()  # 重新输出
        else:
            self.ks833Gen.conf_output_status(new_status)  # 重新配置输出值
            send_pack = self.ks833Gen.pack_adjust_output()  # 调整输出
        return send_pack

    def connect_ks833(self):
        """发送功率源联机握手包"""
        connect_pack = self.ks833Gen.pack_connect()
        self.logger.debug("connect ks833 pack | {}".format(bytesPuttyPrint(connect_pack)))
        self._serial_write(connect_pack)

    def start_ks833_output(self, ks833_status):
        """启动功率源输出"""
        self.ks833Gen.conf_output_status(ks833_status)
        send_pack = self.ks833Gen.pack_start_output()
        self.logger.debug("start output ks833 pack | {}".format(bytesPuttyPrint(send_pack)))
        self._serial_write(send_pack)

    def keep_ks833_output(self, ks833_status):
        """中途修改功率源输出"""
        send_pack = self.gen_output_send_pack(ks833_status)
        self.logger.debug("keep output ks833 pack | {}".format(bytesPuttyPrint(send_pack)))
        self._serial_write(send_pack)

    def stop_ks833_output(self):
        """终止功率源输出"""
        shutdown_pack = self.ks833Gen.pack_shutdown_output()
        self.logger.debug("shutdown ks833 output pack | {}".format(bytesPuttyPrint(shutdown_pack)))
        self._serial_write(shutdown_pack)

    def on_singleShot(self, press):
        """处理单次输出按钮"""
        if press:
            self.commonSetGroupBox.setDisabled(True)
            self.singleShotButton.setText("终止输出")
            self.connect_ks833()
            ks833_status = self.getInputConf()
            self.start_ks833_output(ks833_status)
        else:
            self.commonSetGroupBox.setDisabled(False)
            self.singleShotButton.setText("开始输出")
            self.stop_ks833_output()

    def createCommonSetGroupBox(self):
        """创建常用配置显示构件"""
        self.commonSetGroupBox = QGroupBox("&常用配置")
        layout = QVBoxLayout(self.commonSetGroupBox)
        for i in range(QUEUE_TASK_NUM):
            qb = QRadioButton(self._ks833conf_show_select(KS833_COMMON_ITEM_CONF[i]))
            qb.setCheckable(True)
            qb.setDisabled(True)
            qb.clicked.connect(self.on_commonSetRadio)
            self.commonSetWs.append(qb)
            layout.addWidget(qb)
        self.commonSetButton = QPushButton("进入常用模式")
        self.commonSetButton.setCheckable(True)
        self.commonSetButton.clicked[bool].connect(self.on_commonSetPush)
        layout.addWidget(self.commonSetButton)

    def on_commonSetRadio(self):
        """处理常用设置radio按键按下"""
        if not self.commonSetFlag:
            return
        sender = self.sender()
        if sender not in self.commonSetWs:
            return
        idx = self.commonSetWs.index(sender)
        if idx == self.commonIndex:
            return
        self.logger.debug("sender index is {}".format(idx))
        ks833_status = KS833_COMMON_ITEM_CONF[idx]
        if self.commonIndex < 0:
            self.start_ks833_output(ks833_status)
        else:
            self.keep_ks833_output(ks833_status)
        self.commonIndex = idx

    def on_commonSetPush(self, press):
        if press:
            self.inputGroupBox.setDisabled(True)
            self.commonSetButton.setText("退出常用模式")
            self.commonSetFlag = True
            self.connect_ks833()
            self.ks833Gen.phase_num = 0b111111
            for qb in self.commonSetWs:
                qb.setDisabled(False)
        else:
            self.commonSetFlag = False
            self.inputGroupBox.setDisabled(False)
            self.commonSetButton.setText("进入常用模式")
            self.stop_ks833_output()
            self.commonIndex = -1
            for qb in self.commonSetWs:
                qb.setAutoExclusive(False)
                qb.setChecked(False)
                qb.setAutoExclusive(True)
                qb.setDisabled(True)

    def _ks833conf_show_select(self, ks833_status: KS833_OutputStatus):
        """按钮文字转换显示"""
        return "{0.Ua_Rms} {0.Ub_Rms} {0.Uc_Rms} {0.Ua_Ang} {0.Ub_Ang} {0.Uc_Ang} {0.Ia_Rms} {0.Ib_Rms} {0.Ic_Rms} {0.Ia_Ang} {0.Ib_Ang} {0.Ic_Ang}".format(
            ks833_status
        )

    # 串口设置部分
    def getSerialPorts(self):
        """获取串口列表"""
        self.serialPortList = tuple(serial.tools.list_ports.comports())[::-1]
        if len(self.serialPortList) > 0:
            for serialPort in self.serialPortList:
                yield "{}".format(serialPort.device)
            self.serialObj.port = self.serialPortList[0].device

    def refreshSerialPort(self):
        """刷新串口列表"""
        self.serialPortNameComboBox.clear()
        self.serialPortNameComboBox.addItems(self.getSerialPorts())

    def setSerialPortName(self, serialPortName):
        """设置串口"""
        self.logger.info("serialPortName | {}".format(serialPortName))
        originPort = self.serialObj.port
        newPort = serialPortName.split("|")[0].strip()
        if originPort != newPort:
            self.logger.info("chage serial port | {} --> {}".format(originPort, newPort))
            self.serialObj.port = newPort

    def setSerialPortRate(self, baudrate):
        """串口波特率"""
        self.logger.info("baudrate | {}".format(baudrate))
        self.serialObj.baudrate = int(baudrate)
        self.serialObj.timeout = 3600 / self.serialObj.baudrate

    def setSerialDataBit(self, bytesize):
        """串口数据位长度"""
        self.logger.info("bytesize | {}".format(bytesize))
        self.serialObj.bytesize = int(bytesize)

    def setSerialStopBit(self, stopbits):
        """串口停止位"""
        self.logger.info("stopbits | {}".format(stopbits))
        self.serialObj.stopbits = float(stopbits)

    def setSerialParity(self, parity):
        """串口校验位"""
        self.logger.info("parity | {}".format(parity))
        self.serialObj.parity = parity

    def createSerialWidget(self):
        self.serialWidget = QGroupBox("功率源串口设置")
        serialLayout = QGridLayout(self.serialWidget)
        serialLayout.setContentsMargins(0, 0, 0, 0)
        serialLayout.setSpacing(0)

        self.serialPortNameComboBox = QComboBox()
        self.serialPortNameComboBox.addItems(self.getSerialPorts())
        self.serialPortNameComboBox.activated[str].connect(self.setSerialPortName)
        sserialNameLabel = QLabel("串口:")

        self.serialPortFreshButton = QPushButton("刷新串口")
        self.serialPortFreshButton.clicked[bool].connect(self.refreshSerialPort)

        tempLayout = QHBoxLayout()
        tempLayout.addWidget(sserialNameLabel)
        tempLayout.addWidget(self.serialPortNameComboBox)
        serialLayout.addLayout(tempLayout, 0, 0)
        serialLayout.addWidget(self.serialPortFreshButton, 0, 1)

        self.serialPortRateComboBox = QComboBox()
        self.serialPortRateComboBox.addItems((str(i) for i in BAUD_LIST))
        self.serialPortRateComboBox.setCurrentIndex(BAUD_LIST.index(19200))
        self.setSerialPortRate("19200")
        self.serialPortRateComboBox.activated[str].connect(self.setSerialPortRate)

        self.serialPortDataBitComboBox = QComboBox()
        self.serialPortDataBitComboBox.addItems(["8", "7", "6", "5"])
        self.serialPortDataBitComboBox.activated[str].connect(self.setSerialDataBit)

        self.serialPortStopBitComboBox = QComboBox()
        self.serialPortStopBitComboBox.addItems(["1", "1.5", "2"])
        self.serialPortStopBitComboBox.activated[str].connect(self.setSerialStopBit)

        self.serialPortParityComboBox = QComboBox()
        self.serialPortParityComboBox.setToolTip("N: 无校验\nE: 奇校验\nO: 偶校验\nM: 校验位始终为1\nS: 校验位始终为0")
        self.serialPortParityComboBox.addItems(["N", "E", "O", "M", "S"])
        self.serialPortParityComboBox.activated[str].connect(self.setSerialParity)

        tempLayout = QHBoxLayout()
        tempLayout.addWidget(QLabel("波特率:"))
        tempLayout.addWidget(self.serialPortRateComboBox)
        tempLayout.addWidget(QLabel("数据位:"))
        tempLayout.addWidget(self.serialPortDataBitComboBox)
        serialLayout.addLayout(tempLayout, 1, 0, 1, 2)

        tempLayout = QHBoxLayout()
        tempLayout.addWidget(QLabel("停止位:"))
        tempLayout.addWidget(self.serialPortStopBitComboBox)
        tempLayout.addWidget(QLabel("校验位:"))
        tempLayout.addWidget(self.serialPortParityComboBox)
        tempLayout.addStretch(1)
        serialLayout.addLayout(tempLayout, 2, 0, 1, 2)


if __name__ == "__main__":
    logger = logzero.setup_logger("ks833_ctrl", logfile=None)
    app = QApplication(sys.argv)
    app.setStyle("Windows")
    ks833_ctrl = KS833Ctrler(app=app, logger=logger)
    sys.exit(app.exec_())
